<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>美景烟花</title>

    <style>
      html,
      body {
        margin: 0;
        overflow: hidden;
        width: 100%;
        height: 100%;
        cursor: none;
        background: black;
        background: linear-gradient(to bottom, #000000 0%, #5788fe 100%);
      }

      .filter {
        width: 100%;
        height: 100%;
        position: absolute;
        top: 0;
        left: 0;
        background: #fe5757;
        animation: colorChange 30s ease-in-out infinite;
        animation-fill-mode: both;
        mix-blend-mode: overlay;
      }

      @keyframes colorChange {
        0%,
        100% {
          opacity: 0;
        }
        50% {
          opacity: 0.9;
        }
      }

      .landscape {
        position: absolute;
        bottom: 0px;
        left: 0;
        width: 100%;
        height: 100%;
        /* background-image: url(https://openclipart.org/image/2400px/svg_to_png/250847/Trees-Landscape-Silhouette.png); */
        background-image: url('http://www.jq22.com/css/img/xkbg.png');
        background-size: 1000px 250px;
        background-repeat: repeat-x;
        background-position: center bottom;
      }

      #canvas {
        position: absolute;
      }
    </style>
  </head>
  <body>
    <div class="landscape"></div>
    <div class="filter"></div>
    <canvas id="canvas"></canvas>
    <canvas id="canvas2"></canvas>

    <!-- 星星连线 -->
    <script>
      function Star(id, x, y) {
        this.id = id;
        this.x = x;
        this.y = y;
        this.r = Math.floor(Math.random() * 2) + 1;
        var alpha = (Math.floor(Math.random() * 10) + 1) / 10 / 2;
        this.color = 'rgba(255,255,255,' + alpha + ')';
      }

      Star.prototype.draw = function () {
        ctx.fillStyle = this.color;
        ctx.shadowBlur = this.r * 2;
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI, false);
        ctx.closePath();
        ctx.fill();
      };

      Star.prototype.move = function () {
        this.y -= 0.15;
        if (this.y <= -10) this.y = HEIGHT + 10;
        this.draw();
      };

      Star.prototype.die = function () {
        stars[this.id] = null;
        delete stars[this.id];
      };

      function Dot(id, x, y, r) {
        this.id = id;
        this.x = x;
        this.y = y;
        this.r = Math.floor(Math.random() * 5) + 1;
        this.maxLinks = 2;
        this.speed = 0.5;
        this.a = 0.5;
        this.aReduction = 0.005;
        this.color = 'rgba(255,255,255,' + this.a + ')';
        this.linkColor = 'rgba(255,255,255,' + this.a / 4 + ')';

        this.dir = Math.floor(Math.random() * 140) + 200;
      }

      Dot.prototype.draw = function () {
        ctx.fillStyle = this.color;
        ctx.shadowBlur = this.r * 2;
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI, false);
        ctx.closePath();
        ctx.fill();
      };

      Dot.prototype.link = function () {
        if (this.id == 0) return;
        var previousDot1 = getPreviousDot(this.id, 1);
        var previousDot2 = getPreviousDot(this.id, 2);
        var previousDot3 = getPreviousDot(this.id, 3);
        if (!previousDot1) return;
        ctx.strokeStyle = this.linkColor;
        ctx.moveTo(previousDot1.x, previousDot1.y);
        ctx.beginPath();
        ctx.lineTo(this.x, this.y);
        if (previousDot2 != false) ctx.lineTo(previousDot2.x, previousDot2.y);
        if (previousDot3 != false) ctx.lineTo(previousDot3.x, previousDot3.y);
        ctx.stroke();
        ctx.closePath();
      };

      function getPreviousDot(id, stepback) {
        if (id == 0 || id - stepback < 0) return false;
        if (typeof dots[id - stepback] != 'undefined') return dots[id - stepback];
        else return false; //getPreviousDot(id - stepback);
      }

      Dot.prototype.move = function () {
        this.a -= this.aReduction;
        if (this.a <= 0) {
          this.die();
          return;
        }
        this.color = 'rgba(255,255,255,' + this.a + ')';
        this.linkColor = 'rgba(255,255,255,' + this.a / 4 + ')';
        (this.x = this.x + Math.cos(degToRad(this.dir)) * this.speed),
          (this.y = this.y + Math.sin(degToRad(this.dir)) * this.speed);

        this.draw();
        this.link();
      };

      Dot.prototype.die = function () {
        dots[this.id] = null;
        delete dots[this.id];
      };

      var canvas2 = document.getElementById('canvas'),
        ctx = canvas2.getContext('2d'),
        WIDTH,
        HEIGHT,
        mouseMoving = false,
        mouseMoveChecker,
        mouseX,
        mouseY,
        stars = [],
        initStarsPopulation = 80,
        dots = [],
        dotsMinDist = 2,
        maxDistFromCursor = 50;

      setCanvasSize();
      init();

      function setCanvasSize() {
        (WIDTH = document.documentElement.clientWidth),
          (HEIGHT = document.documentElement.clientHeight);

        canvas2.setAttribute('width', WIDTH);
        canvas2.setAttribute('height', HEIGHT);
      }

      function init() {
        ctx.strokeStyle = 'white';
        ctx.shadowColor = 'white';
        for (var i = 0; i < initStarsPopulation; i++) {
          stars[i] = new Star(
            i,
            Math.floor(Math.random() * WIDTH),
            Math.floor(Math.random() * HEIGHT)
          );
          //stars[i].draw();
        }
        ctx.shadowBlur = 0;
        animate();
      }

      function animate() {
        ctx.clearRect(0, 0, WIDTH, HEIGHT);

        for (var i in stars) {
          stars[i].move();
        }
        for (var i in dots) {
          dots[i].move();
        }
        drawIfMouseMoving();
        requestAnimationFrame(animate);
      }

      window.onmousemove = function (e) {
        mouseMoving = true;
        mouseX = e.clientX;
        mouseY = e.clientY;
        clearInterval(mouseMoveChecker);
        mouseMoveChecker = setTimeout(function () {
          mouseMoving = false;
        }, 100);
      };

      function drawIfMouseMoving() {
        if (!mouseMoving) return;

        if (dots.length == 0) {
          dots[0] = new Dot(0, mouseX, mouseY);
          dots[0].draw();
          return;
        }

        var previousDot = getPreviousDot(dots.length, 1);
        var prevX = previousDot.x;
        var prevY = previousDot.y;

        var diffX = Math.abs(prevX - mouseX);
        var diffY = Math.abs(prevY - mouseY);

        if (diffX < dotsMinDist || diffY < dotsMinDist) return;

        var xVariation = Math.random() > 0.5 ? -1 : 1;
        xVariation = xVariation * Math.floor(Math.random() * maxDistFromCursor) + 1;
        var yVariation = Math.random() > 0.5 ? -1 : 1;
        yVariation = yVariation * Math.floor(Math.random() * maxDistFromCursor) + 1;
        dots[dots.length] = new Dot(dots.length, mouseX + xVariation, mouseY + yVariation);
        dots[dots.length - 1].draw();
        dots[dots.length - 1].link();
      }
      //setInterval(drawIfMouseMoving, 17);

      function degToRad(deg) {
        return deg * (Math.PI / 180);
      }
    </script>

    <!-- 创建烟花 -->
    <script>
      class Vector2 {
        constructor(x = 0, y = 0) {
          this.x = x;
          this.y = y;
        }

        add(v) {
          this.x += v.x;
          this.y += v.y;
          return this;
        }

        multiplyScalar(s) {
          this.x *= s;
          this.y *= s;
          return this;
        }

        clone() {
          return new Vector2(this.x, this.y);
        }
      }

      class Time {
        constructor() {
          const now = Time.now();

          this.delta = 0;
          this.elapsed = 0;
          this.start = now;
          this.previous = now;
        }

        update() {
          const now = Time.now();

          this.delta = now - this.previous;
          this.elapsed = now - this.start;
          this.previous = now;
        }

        static now() {
          return Date.now() / 1000;
        }
      }

      class Particle {
        constructor(
          position,
          velocity = new Vector2(),
          color = 'white',
          radius = 1,
          lifetime = 1,
          mass = 1
        ) {
          this.position = position;
          this.velocity = velocity;
          this.color = color;
          this.radius = radius;
          this.lifetime = lifetime;
          this.mass = mass;

          this.isInCanvas = true;
          this.createdOn = Time.now();
        }

        update(time) {
          if (!this.getRemainingLifetime()) {
            return;
          }

          this.velocity.add(Particle.GRAVITATION.clone().multiplyScalar(this.mass));
          this.position.add(this.velocity.clone().multiplyScalar(time.delta));
        }

        render(canvas, context) {
          const remainingLifetime = this.getRemainingLifetime();

          if (!remainingLifetime) return;

          const radius = this.radius * remainingLifetime;

          context.globalAlpha = remainingLifetime;
          context.globalCompositeOperation = 'lighter';
          context.fillStyle = this.color;

          context.beginPath();
          context.arc(this.position.x, this.position.y, radius, 0, Math.PI * 2);
          context.fill();
        }

        getRemainingLifetime() {
          const elapsedLifetime = Time.now() - this.createdOn;
          return Math.max(0, this.lifetime - elapsedLifetime) / this.lifetime;
        }
      }

      Particle.GRAVITATION = new Vector2(0, 9.81);

      class Trail extends Particle {
        constructor(childFactory, position, velocity = new Vector2(), lifetime = 1, mass = 1) {
          super(position, velocity);

          this.childFactory = childFactory;
          this.children = [];
          this.lifetime = lifetime;
          this.mass = mass;

          this.isAlive = true;
        }

        update(time) {
          super.update(time);

          // Add a new child on every frame
          if (this.isAlive && this.getRemainingLifetime()) {
            this.children.push(this.childFactory(this));
          }

          // Remove particles that are dead
          this.children = this.children.filter(function (child) {
            if (child instanceof Trail) {
              return child.isAlive;
            }

            return child.getRemainingLifetime();
          });

          // Kill trail if all particles fade away
          if (!this.children.length) {
            this.isAlive = false;
          }

          // Update particles
          this.children.forEach(function (child) {
            child.update(time);
          });
        }

        render(canvas, context) {
          // Render all children
          this.children.forEach(function (child) {
            child.render(canvas, context);
          });
        }
      }

      class Rocket extends Trail {
        constructor(childFactory, explosionFactory, position, velocity = new Vector2()) {
          super(childFactory, position, velocity);

          this.explosionFactory = explosionFactory;
          this.lifetime = 10;
        }

        update(time) {
          if (this.getRemainingLifetime() && this.velocity.y > 0) {
            this.explosionFactory(this);
            this.lifetime = 0;
          }

          super.update(time);
        }
      }

      const canvas = document.getElementById('canvas2');
      const context = canvas.getContext('2d');
      const time = new Time();
      let rockets = [];

      const getTrustParticleFactory = function (baseHue) {
        function getColor() {
          const hue = Math.floor(Math.random() * 15 + 30);
          return `hsl(${hue}, 100%, 75%`;
        }

        return function (parent) {
          const position = this.position.clone();
          const velocity = this.velocity.clone().multiplyScalar(-0.1);
          velocity.x += (Math.random() - 0.5) * 8;
          const color = getColor();
          const radius = 1 + Math.random();
          const lifetime = 0.5 + Math.random() * 0.5;
          const mass = 0.01;

          return new Particle(position, velocity, color, radius, lifetime, mass);
        };
      };

      const getExplosionFactory = function (baseHue) {
        function getColor() {
          const hue = Math.floor(baseHue + Math.random() * 15) % 360;
          const lightness = Math.floor(Math.pow(Math.random(), 2) * 50 + 50);
          return `hsl(${hue}, 100%, ${lightness}%`;
        }

        function getChildFactory() {
          return function (parent) {
            const direction = Math.random() * Math.PI * 2;
            const force = 8;
            const velocity = new Vector2(Math.cos(direction) * force, Math.sin(direction) * force);
            const color = getColor();
            const radius = 1 + Math.random();
            const lifetime = 1;
            const mass = 0.1;

            return new Particle(parent.position.clone(), velocity, color, radius, lifetime, mass);
          };
        }

        function getTrail(position) {
          const direction = Math.random() * Math.PI * 2;
          const force = Math.random() * 128;
          const velocity = new Vector2(Math.cos(direction) * force, Math.sin(direction) * force);
          const lifetime = 0.5 + Math.random();
          const mass = 0.075;

          return new Trail(getChildFactory(), position, velocity, lifetime, mass);
        }

        return function (parent) {
          let trails = 32;
          while (trails--) {
            parent.children.push(getTrail(parent.position.clone()));
          }
        };
      };

      const addRocket = function () {
        const trustParticleFactory = getTrustParticleFactory();
        const explosionFactory = getExplosionFactory(Math.random() * 360);

        const position = new Vector2(Math.random() * canvas.width, canvas.height);
        const thrust = window.innerHeight * 0.75;
        const angle = Math.PI / -2 + ((Math.random() - 0.5) * Math.PI) / 8;
        const velocity = new Vector2(Math.cos(angle) * thrust, Math.sin(angle) * thrust);
        const lifetime = 3;

        rockets.push(
          new Rocket(trustParticleFactory, explosionFactory, position, velocity, lifetime)
        );

        rockets = rockets.filter(function (rocket) {
          return rocket.isAlive;
        });
      };

      const render = function () {
        requestAnimationFrame(render);

        time.update();
        context.clearRect(0, 0, canvas.width, canvas.height);

        rockets.forEach(function (rocket) {
          rocket.update(time);
          rocket.render(canvas, context);
        });
      };

      const resize = function () {
        canvas.height = window.innerHeight;
        canvas.width = window.innerWidth;
      };

      canvas.onclick = addRocket;

      window.onresize = resize;
      resize();

      setInterval(addRocket, 500);
      render();
    </script>
  </body>
</html>
